package org.swift.mashup.engine.model;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.transform.TransformerConfigurationException;

import net.sf.saxon.om.NamespaceResolver;

import org.swift.commons.client.Call;
import org.swift.commons.util.StringUtils;
import org.swift.commons.xml.XmlUtils;
import org.swift.commons.xml.XmlUtils.Closure;
import org.swift.commons.xml.vtd.VTDUtil;
import org.swift.mashup.engine.InitializationException;
import org.swift.mashup.engine.api.ScriptEngineProvider;
import org.swift.mashup.engine.concurrency.agenda.ActionTemplate;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class MashupXmlActionTemplateParser implements
		ActionTemplateParser<Node> {
	private final static String COMPUTE_PROP_PREFIX = "mashup.script";
	private static final String REST_KEY = "{restparam}";
	private static final String REST_LOCATION = "path";
	private static final String HEADER_KEY = "{headerparam}";
	private static final String HEADER_LOCATION = "header";

	private final OctaveScriptValidator octaveScriptValidator = new OctaveScriptValidator();

	private WebServiceRepository serviceRepository;
	private ScriptEngineProvider scriptEngineProvider;

	public MashupXmlActionTemplateParser() {
		this.serviceRepository = WebServiceRepository.getInstance();
		this.serviceRepository.setAutoLoad(true);
	}

	public MashupXmlActionTemplateParser(WebServiceRepository serviceRepository) {
		this.serviceRepository = serviceRepository;
	}

	public MashupXmlActionTemplateParser(
			ScriptEngineProvider scriptEngineProvider) {
		this.serviceRepository = WebServiceRepository.getInstance();
		this.scriptEngineProvider = scriptEngineProvider;
	}

	public boolean isParseable(Node item) {
		String type = this.loadType(item);
		return ("grab".equals(type) || "publish".equals(type));
	}

	public ActionTemplate createActionTemplate(Node node)
			throws InitializationException, ParseException {
		Map<String, String> namespaces = VTDUtil.extractNamespaces(XmlUtils
				.toString(node).getBytes());
		String type = this.loadType(node);
		String name = this.loadName(node);
		String encoding = this.loadEncoding(node);
		Map<String, With> withs = loadWiths(node);
		Map<String, Constant> constants = loadConstants(node);
		Collection<When> whens = loadWhens(node, constants, namespaces);
		From from = loadFrom(node, name, withs);
		ParameterProcessor paramProcessor = loadParamProcessor(from, withs
				.keySet(), node, namespaces);
		Set<Parameter> parameters = new HashSet<Parameter>();
		parameters.addAll(withs.values());
		parameters.addAll(constants.values());
		try {
			Activity activity = this.loadActivity(node, parameters, namespaces);
			GrabActionTemplate actionTemplate = new GrabActionTemplate(name,
					from, activity, withs, paramProcessor, whens, constants, encoding);
			actionTemplate.setPublish("publish".equals(type));
			return actionTemplate;
		} catch (TransformerConfigurationException tce) {
			throw new ParseException("The XSLT is not valid on node \"" + name
					+ "\"", tce);
		} catch (SAXException se) {
			throw new ParseException("Invalid XML found on node \"" + name
					+ "\": " + se.getMessage(), se);
		} catch (ScriptError se) {
			throw new ParseException("Script syntax error on node \"" + name
					+ "\": " + se.getMessage(), se);
		}
	}

	protected Activity loadActivity(Node node,
			Collection<? extends Parameter> params,
			Map<String, String> namespaces)
			throws TransformerConfigurationException, SAXException, ScriptError {
		String matchingString = loadMatchingString(node);
		String language = node.getAttributes().getNamedItem("language") != null ? node
				.getAttributes().getNamedItem("language").getTextContent()
				: "XSLT";
		Collection<Node> returnNodes = findReturnElements(node.getChildNodes());
		String returnType = null;
		NodeList returnContents = null;
		Activity activity;
		if (returnNodes.size() <= 1){
			if (returnNodes.size() > 0){
				Node returnNode = returnNodes.iterator().next();
				returnType = loadReturnType(returnNode);
				returnContents = returnNode.getChildNodes();
			}
			activity = buildSingleActivity(node, params, namespaces, language, matchingString, returnContents);
			if (returnType == null){
				return activity;
			}
			ActivitySet activitySet = new ActivitySet();
			activitySet.addActivity(returnType, activity);
			return activitySet;
		} else {
			ActivitySet activitySet = new ActivitySet();
			for (Node returnNode : returnNodes){
				returnType = loadReturnType(returnNode);
				returnContents = returnNode.getChildNodes();
				activity = buildSingleActivity(node, params, namespaces, language, matchingString, returnContents);
				activitySet.addActivity(returnType, activity);
			}
			return activitySet;
		}
		//NodeList returnContents = findReturn(node.getChildNodes());
	}
	
	protected Activity buildSingleActivity (Node node,
			Collection<? extends Parameter> params,
			Map<String, String> namespaces,
			final String language,
			final String matchingString,
			final NodeList returnContents) throws TransformerConfigurationException, SAXException, ScriptError {
		Node encoding = node.getAttributes().getNamedItem("encoding");
		String encodingString = null;
		if(encoding!=null){
			encodingString = encoding.getNodeValue();
		}
		if (matchingString == null && returnContents == null) {
			return NullActivity.instance();
		}
		if (language == null || "XSLT".equals(language) || "".equals(language)) {
			return createXmlActivity(returnContents, matchingString, params,
					namespaces, encodingString);
		} else if ("XQuery".equals(language)) {
			return new XQueryActivity(this.loadName(node), returnContents,
					matchingString, params, namespaces);
		} else if ("joctave".equals(language)) {
			final String script = getScript(returnContents);
			octaveScriptValidator.validateScript(script);
			return new ScriptEngineActivity(this
					.getOctaveScriptEngineProvider(), script);
		} else {
			try {
				final String script = getScript(returnContents);
				final List<String> cmd = getCommand(language);
				final File workingDir = getWorkDir(language);
				return new ScriptActivity(script, cmd, workingDir);
			} catch (UndefinedProgramException e) {
				return new JSR223ScriptActivity(language, getScript(returnContents));
			}
		}
	}

	private Activity createXmlActivity(NodeList returnContents,
			String matchingString, Collection<? extends Parameter> params,
			Map<String, String> namespaces, String encoding)
			throws TransformerConfigurationException, SAXException {

		TransformXsltActivity transformActivity = null;

		if (returnContents != null) {
			transformActivity = new TransformXsltActivity(returnContents,
					matchingString, params, namespaces, encoding);
		}

		final XMLActivity activity;

		if (matchingString != null) {
			activity = new MatchActivity(matchingString, params, namespaces);
			((MatchActivity) activity).setNextActivity(transformActivity);
		} else { // returnContents != null {
			activity = transformActivity;
		}

		final String workingDirectory = System
				.getProperty("org.swift.mashup.engine.concurrency.WorkingDirectory");
		if (workingDirectory != null) {
			activity.setWorkingDirectory(workingDirectory);
		}

		return activity;
	}

	private String getScript(NodeList nodeList) {
		for (int i = 0; i < nodeList.getLength(); i++) {
			String value = nodeList.item(i).getNodeValue();
			if (value != null && !"".equals(value.trim())) {
				return value;
			}
		}
		return "";
	}

	// protected for testing
	protected File getWorkDir(String language) {
		String workDir = System.getProperty(COMPUTE_PROP_PREFIX + "."
				+ language + ".workingDirectory");
		if (workDir == null)
			return null;
		return new File(workDir);
	}

	// protected for testing
	protected List<String> getCommand(String language) {
		String program = System.getProperty(COMPUTE_PROP_PREFIX + "."
				+ language);
		if (program == null) {
			throw new UndefinedProgramException(COMPUTE_PROP_PREFIX + "." + language
					+ " was not set");
		}

		List<String> list = new ArrayList<String>();
		list.add(program);

		final String argPrefix = COMPUTE_PROP_PREFIX + "." + language + ".arg";
		String arg = System.getProperty(argPrefix + ".1");
		for (int i = 2; arg != null; i++) {
			list.add(arg);
			arg = System.getProperty(argPrefix + "." + i);
		}
		list.add("script.m");
		return list;
	}

	protected String loadType(Node node) {
		return node.getLocalName();
	}

	protected String loadOperation(Node node) {
		return getOptionalAttribute(node, "operation");
	}

	protected String loadMethod(Node node) {
		return getOptionalAttribute(node, "method");
	}

	protected String loadContentType(Node node) {
		return getOptionalAttribute(node, "contentType");
	}

	protected String loadMatchingString(Node node) {
		return getOptionalAttribute(node, "matching");
	}
	
	protected String loadReturnType (Node returnNode){
		return getOptionalAttribute(returnNode, "type");
	}

	protected String loadFromString(Node node) {
		return getOptionalAttribute(node, "from");
	}

	protected String loadFromProcessor(Node node) {
		return getOptionalAttribute(node, "fromProcessor");
	}

	protected String loadName(Node node) {
		return node.getAttributes().getNamedItem("name").getTextContent();
	}

	protected String loadEncoding(Node node) {
		return getOptionalAttribute(node, "encoding");
	}

	protected String loadFromCacheDuration(Node node) {
		String cacheDuaration = getOptionalAttribute(node, "cacheDuration");
		if(StringUtils.isBlank(cacheDuaration)){
			cacheDuaration = getOptionalAttribute(node.getOwnerDocument().getFirstChild(), "cacheDuration");
		}
		return cacheDuaration;
	}

	protected String getOptionalAttribute(Node node, String name) {
		return (node.getAttributes().getNamedItem(name) != null) ? node
				.getAttributes().getNamedItem(name).getTextContent() : null;
	}

	protected From loadFrom(Node node, String name, Map<String, With> withs)
			throws InitializationException {
		String fromString = this.loadFromString(node);
		String operationString = this.loadOperation(node);
		String methodString = this.loadMethod(node);
		String contentType = this.loadContentType(node);
		String fromProcessorString = this.loadFromProcessor(node);

		String username = null;
		String password = null;
		String cacheDurationString = this.loadFromCacheDuration(node);
		Node authNode = findAuth(node.getChildNodes());
		if(authNode!=null){
			username = getOptionalAttribute(authNode, "username");
			password = getOptionalAttribute(authNode, "password");
		}
		From from = null;

		boolean isRemoteFrom = !StringUtils.isBlank(methodString)
				|| (fromString != null && fromString.startsWith("http"));

		if ( fromString == null || (StringUtils.isBlank(fromString) && !isRemoteFrom) ) {
			return NullFrom.instance();
		} else if ( isRemoteFrom ) {
			try {
				URL fromURL = !fromString.isEmpty() ? new URL(fromString) : null;
				Call call;
				if (operationString == null) {
					if (methodString == null) {
						methodString = "GET";
					}
					call = this.serviceRepository.createRestCall(fromURL,
							methodString, contentType, username, password);
				} else {
					call = this.serviceRepository.createSoapCall(fromURL,
							operationString);
				}
				if(!StringUtils.isBlank(cacheDurationString)){
					from = new RemoteFrom(fromString, call, new Integer(cacheDurationString));
				}else{
					from = new RemoteFrom(fromString, call);
				}
			} catch (MalformedURLException e) {
				throw new RuntimeException(e);
			}
		} else {
			// if this is a from variable, we must register the from as a key.
			withs.put(name, new With(name, fromString));
			from = new FromVariable(name, fromString);
		}
		from.setFromProcessor(fromProcessorString);
		return from;
	}

	protected Map<String, With> loadWiths(Node grabNode) {
		NodeList childNodes = grabNode.getChildNodes();

		Map<String, With> map = new HashMap<String, With>();
		Node node;
		for (int i = 0; i < childNodes.getLength(); i++) {
			node = childNodes.item(i);
			if (Node.ELEMENT_NODE == node.getNodeType()
					&& MashupXmlActionConfig.NAMESPACE.equals(node
							.getNamespaceURI())
					&& "with".equals(node.getLocalName())) {
				Element element = (Element) node;
				if (element.getAttributeNode("from") != null) {
					map.put(element.getAttribute("name"),
							new With(element.getAttribute("name"), element
									.getAttribute("from")));
				}
			}
		}
		return map;
	}

	protected ParameterProcessor loadParamProcessor(From from,
			final Collection<String> withNames, Node grabNode,
			final Map<String, String> namespaces) {
		final XQueryParameterProcessor paramProcessor;
		final NamespaceResolver resolver = new NamespaceResolverImpl(namespaces);
		if (from instanceof FromVariable) {
			String fromVar = from.getSource();
			Collection<String> customNames = new ArrayList<String>();
			customNames.addAll(withNames);
			customNames.add(fromVar);
			paramProcessor = new XQueryParameterProcessor(customNames, resolver);
			paramProcessor.addParameterFromWith(fromVar, fromVar);
			return paramProcessor;
		} else {
			NodeList childNodes = grabNode.getChildNodes();
			Node node;
			for (int i = 0; i < childNodes.getLength(); i++) {
				node = childNodes.item(i);
				if (Node.ELEMENT_NODE == node.getNodeType()
						&& MashupXmlActionConfig.NAMESPACE.equals(node
								.getNamespaceURI())
						&& "sendParameters".equals(node.getLocalName())) {
					paramProcessor = new XQueryParameterProcessor(withNames,
							resolver);
					XmlUtils.eachElement(node.getChildNodes(),
							new Closure<Element>() {
								public void execute(Element item) {
									String name = item.getAttribute("name");
									String expression = item
											.getAttribute("path");
									String location = item.getAttribute("location");
									if(!StringUtils.isBlank(location) && location.equals(REST_LOCATION)){
										name = REST_KEY + name;
									}
									if(!StringUtils.isBlank(location) && location.equals(HEADER_LOCATION)){
										name = HEADER_KEY + name;
									}
									if (expression.startsWith("$")
											&& withNames.contains(expression
													.substring(1))) {
										paramProcessor.addParameterFromWith(
												name, expression.substring(1));
									} else {
										paramProcessor.addParameterSelector(
												name, expression);
									}
								}
							});
					return paramProcessor;
				}
			}
			return NullParameterProcessor.Instance;
		}
	}

	protected Collection<When> loadWhens(Node grabNode,
			Map<String, Constant> constants,
			Map<String, String> namespaces) {

		NodeList childNodes = grabNode.getChildNodes();
		Set<When> set = new HashSet<When>();
		Node node;
		for (int i = 0; i < childNodes.getLength(); i++) {
			node = childNodes.item(i);
			if (Node.ELEMENT_NODE == node.getNodeType()
					&& MashupXmlActionConfig.NAMESPACE.equals(node
							.getNamespaceURI())
					&& "when".equals(node.getLocalName())) {
				Element element = (Element) node;
				set.add(new When(element.getAttribute("condition"), constants, namespaces));
			}
		}
		return set;
	}

	protected Map<String, Constant> loadConstants(Node grabNode) {
		NodeList childNodes = grabNode.getChildNodes();

		Map<String, Constant> map = new HashMap<String, Constant>();
		Node node;
		for (int i = 0; i < childNodes.getLength(); i++) {
			node = childNodes.item(i);
			if (Node.ELEMENT_NODE == node.getNodeType()
					&& MashupXmlActionConfig.NAMESPACE.equals(node
							.getNamespaceURI())
					&& "with".equals(node.getLocalName())) {
				Element element = (Element) node;
				if (element.getAttributeNode("value") != null) {
					map.put(element.getAttribute("name"), new Constant(element
							.getAttribute("name"), element
							.getAttribute("value")));
				}
			}
		}
		return map;
	}
	
	protected Collection<Node> findReturnElements (NodeList inNodes){
		Collection<Node> returnNodes = new ArrayList<Node>();
		Node node;
		for (int i = 0; i < inNodes.getLength(); i++) {
			node = inNodes.item(i);
			if (Node.ELEMENT_NODE == node.getNodeType()
					&& MashupXmlActionConfig.NAMESPACE.equals(node
							.getNamespaceURI())
					&& "return".equals(node.getLocalName())) {
				returnNodes.add(node);
			}
		}
		return returnNodes;
	}

	/**
	 * Grabs any any xml inside our return
	 * 
	 * @param nodes
	 * @return
	 */
	protected NodeList findReturn(NodeList nodes) {
		Node node;
		for (int i = 0; i < nodes.getLength(); i++) {
			node = nodes.item(i);
			if (Node.ELEMENT_NODE == node.getNodeType()
					&& MashupXmlActionConfig.NAMESPACE.equals(node
							.getNamespaceURI())
					&& "return".equals(node.getLocalName())) {
				return node.getChildNodes();
			}
		}
		return null;
	}
	
	private Node findAuth(NodeList nodes){
		Node node;
		for (int i = 0; i < nodes.getLength(); i++) {
			node = nodes.item(i);
			if (Node.ELEMENT_NODE == node.getNodeType()
					&& MashupXmlActionConfig.NAMESPACE.equals(node
							.getNamespaceURI())
					&& "auth".equals(node.getLocalName())) {
				return node;
			}
		}
		return null;
	}

	public ScriptEngineProvider getOctaveScriptEngineProvider() {
		if (this.scriptEngineProvider == null) {
			this.scriptEngineProvider = NamedScriptEngineProvider
					.getSharedEngineProvider("pool/OctaveEnginePool");
		}
		return scriptEngineProvider;
	}

	public void setOctaveScriptEngineProvider(
			ScriptEngineProvider scriptEngineProvider) {
		this.scriptEngineProvider = scriptEngineProvider;
	}

	protected class UndefinedProgramException extends RuntimeException {
		public UndefinedProgramException(String message) {
			super(message);
		}
	}
}
